{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.messages import HumanMessage, BaseMessage\n",
    "from langgraph.graph import END, MessageGraph\n",
    "import json\n",
    "from langchain_core.messages import ToolMessage\n",
    "from langchain_core.tools import tool\n",
    "from langchain_core.utils.function_calling import convert_to_openai_tool\n",
    "from typing import List\n",
    "from langchain.callbacks import StdOutCallbackHandler"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "handler = StdOutCallbackHandler()\n",
    "model = ChatOpenAI(api_key=\"sk-uRUknVzF2daRmidJ227fEa256c5d4f4b9f9a7bB6CeCd0176\", \n",
    "                 base_url=\"https://api.openai-hub.com/v1\", \n",
    "                 callbacks=[handler])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "model_with_tools bound=ChatOpenAI(callbacks=[<langchain_core.callbacks.stdout.StdOutCallbackHandler object at 0x10b618310>], client=<openai.resources.chat.completions.Completions object at 0x10b621040>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x10b6292e0>, openai_api_key=SecretStr('**********'), openai_api_base='https://api.openai-hub.com/v1', openai_proxy='') kwargs={'tools': [{'type': 'function', 'function': {'name': 'multiply', 'description': 'Multiplies two numbers together.', 'parameters': {'type': 'object', 'properties': {'first_number': {'type': 'integer'}, 'second_number': {'type': 'integer'}}, 'required': ['first_number', 'second_number']}}}]}\n",
      "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n",
      "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> HumanMessage(content='What is 123 * 456?')\n",
      "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n",
      "\u001b[0m- \u001b[33;1m\u001b[1;3m__root__\u001b[0m -> HumanMessage(content='What is 123 * 456?')\n",
      "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n",
      "\u001b[0m- \u001b[32;1m\u001b[1;3moracle\u001b[0m -> [HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b')]\n",
      "invoke_model [HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b')]\n",
      "router [HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fb5cd881-d025-4dfb-8431-713d34ac9909-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q'}])]\n",
      "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n",
      "\u001b[0m- \u001b[33;1m\u001b[1;3m__root__\u001b[0m -> AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fb5cd881-d025-4dfb-8431-713d34ac9909-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q'}])\n",
      "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n",
      "\u001b[0m- \u001b[32;1m\u001b[1;3mmultiply\u001b[0m -> [HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b'),\n",
      " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fb5cd881-d025-4dfb-8431-713d34ac9909-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q'}])]\n",
      "invoke_tool [HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fb5cd881-d025-4dfb-8431-713d34ac9909-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q'}])]\n",
      "invoke_tool_1 {'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}\n",
      "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n",
      "\u001b[0m- \u001b[33;1m\u001b[1;3m__root__\u001b[0m -> ToolMessage(content='56088', tool_call_id='call_OAu6OtKNBSgqPWRHpr9CWx0Q')\n",
      "[HumanMessage(content='What is 123 * 456?', id='f9b8e50a-509c-4843-afc2-f720382c916b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fb5cd881-d025-4dfb-8431-713d34ac9909-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_OAu6OtKNBSgqPWRHpr9CWx0Q'}]), ToolMessage(content='56088', id='90ff939d-5672-4983-8b59-b37918b3ad8b', tool_call_id='call_OAu6OtKNBSgqPWRHpr9CWx0Q')]\n"
     ]
    }
   ],
   "source": [
    "@tool\n",
    "def multiply(first_number: int, second_number: int):\n",
    "    \"\"\"Multiplies two numbers together.\"\"\"\n",
    "    return first_number * second_number\n",
    "\n",
    "model_with_tools = model.bind(tools=[convert_to_openai_tool(multiply)])\n",
    "print(\"model_with_tools\", model_with_tools)\n",
    "\n",
    "graph = MessageGraph()\n",
    "\n",
    "def invoke_model(state: List[BaseMessage]):\n",
    "    print(\"invoke_model\", state)\n",
    "    return model_with_tools.invoke(state)\n",
    "\n",
    "def invoke_tool(state: List[BaseMessage]):\n",
    "    print(\"invoke_tool\", state)\n",
    "\n",
    "    tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n",
    "    multiply_call = None\n",
    "\n",
    "    for tool_call in tool_calls:\n",
    "        if tool_call.get(\"function\").get(\"name\") == \"multiply\":\n",
    "            multiply_call = tool_call\n",
    "\n",
    "    if multiply_call is None:\n",
    "        raise Exception(\"No adder input found.\")\n",
    "\n",
    "    print(\"invoke_tool_1\", multiply_call)\n",
    "    res = multiply.invoke(\n",
    "        json.loads(multiply_call.get(\"function\").get(\"arguments\"))\n",
    "    )\n",
    "\n",
    "    return ToolMessage(\n",
    "        tool_call_id=multiply_call.get(\"id\"),\n",
    "        content=res\n",
    "    )\n",
    "\n",
    "graph.add_node(\"oracle\", invoke_model)\n",
    "\n",
    "graph.add_node(\"multiply\", invoke_tool)\n",
    "\n",
    "graph.add_edge(\"multiply\", END)\n",
    "\n",
    "graph.set_entry_point(\"oracle\")\n",
    "\n",
    "def router(state: List[BaseMessage]):\n",
    "    print(\"router\", state)\n",
    "    tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n",
    "    if len(tool_calls):\n",
    "        return \"multiply\"\n",
    "    else:\n",
    "        return \"end\"\n",
    "\n",
    "graph.add_conditional_edges(\"oracle\", router, {\n",
    "    \"multiply\": \"multiply\",\n",
    "    \"end\": END,\n",
    "})\n",
    "\n",
    "runnable = graph.compile()\n",
    "\n",
    "response = runnable.invoke(HumanMessage(\"What is 123 * 456?\"), debug=True)\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "56088\n"
     ]
    }
   ],
   "source": [
    "@tool\n",
    "def multiply1(first_number: int, second_number: int):\n",
    "    \"\"\"Multiplies two numbers together.\"\"\"\n",
    "    return first_number * second_number\n",
    "\n",
    "res = multiply1.invoke(\n",
    "        json.loads('{\"first_number\": 123, \"second_number\": 456}')\n",
    "    )\n",
    "\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/mac/Library/Python/3.9/lib/python/site-packages/langchain_core/_api/deprecation.py:139: LangChainDeprecationWarning: The function `format_tool_to_openai_function` was deprecated in LangChain 0.1.16 and will be removed in 0.3.0. Use langchain_core.utils.function_calling.convert_to_openai_function() instead.\n",
      "  warn_deprecated(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 87, 'total_tokens': 108}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'function_call', 'logprobs': None}, id='run-a6f7c9f4-1fc8-45b2-8567-a0a3520b3b0a-0'), FunctionMessage(content=\"[{'url': 'https://www.wunderground.com/hourly/us/ca/san-francisco/94104/date/2024-6-28', 'content': 'Current Weather for Popular Cities . San Francisco, CA 55 ° F Partly Cloudy; Manhattan, NY 70 ° F Clear; Schiller Park, IL (60176) warning 75 ° F Mostly Cloudy; Boston, MA 65 ° F Fair; Houston ...'}]\", name='tavily_search_results_json'), AIMessage(content='The current weather in San Francisco is 55°F with partly cloudy skies. For more detailed information, you can visit [this link](https://www.wunderground.com/hourly/us/ca/san-francisco/94104/date/2024-6-28).', response_metadata={'token_usage': {'completion_tokens': 57, 'prompt_tokens': 217, 'total_tokens': 274}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-a6d8ae91-b6ba-4d05-b120-f0d9d3c9d755-0')]}\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "import os\n",
    "\n",
    "os.environ[\"TAVILY_API_KEY\"] = \"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\"\n",
    "\n",
    "tools = [TavilySearchResults(max_results=1, tavily_api_key=\"xxx\")]\n",
    "\n",
    "from langgraph.prebuilt import ToolExecutor\n",
    "\n",
    "tool_executor = ToolExecutor(tools)\n",
    "\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# We will set streaming=True so that we can stream tokens\n",
    "# See the streaming section for more information on this.\n",
    "#model = ChatOpenAI(temperature=0)\n",
    "\n",
    "from langchain.tools.render import format_tool_to_openai_function\n",
    "\n",
    "functions = [format_tool_to_openai_function(t) for t in tools]\n",
    "model = model.bind_functions(functions)\n",
    "\n",
    "from typing import TypedDict, Annotated, Sequence\n",
    "import operator\n",
    "from langchain_core.messages import BaseMessage\n",
    "\n",
    "\n",
    "class AgentState(TypedDict):\n",
    "    messages: Annotated[Sequence[BaseMessage], operator.add]\n",
    "    \n",
    "from langgraph.prebuilt import ToolInvocation\n",
    "import json\n",
    "from langchain_core.messages import FunctionMessage\n",
    "\n",
    "# Define the function that determines whether to continue or not\n",
    "def should_continue(state):\n",
    "    messages = state['messages']\n",
    "    last_message = messages[-1]\n",
    "    if \"function_call\" not in last_message.additional_kwargs:\n",
    "        return \"end\"\n",
    "    else:\n",
    "        return \"continue\"\n",
    "\n",
    "# Define the function that calls the model\n",
    "def call_model(state):\n",
    "    messages = state['messages']\n",
    "    response = model.invoke(messages)\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "# Define the function to execute tools\n",
    "def call_tool(state):\n",
    "    messages = state['messages']\n",
    "    last_message = messages[-1]\n",
    "    action = ToolInvocation(\n",
    "        tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n",
    "        tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n",
    "    )\n",
    "    response = tool_executor.invoke(action)\n",
    "    function_message = FunctionMessage(content=str(response), name=action.tool)\n",
    "    return {\"messages\": [function_message]}\n",
    "\n",
    "from langgraph.graph import StateGraph, END\n",
    "# Define a new graph\n",
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"action\", call_tool)\n",
    "workflow.set_entry_point(\"agent\")\n",
    "\n",
    "workflow.add_conditional_edges(\n",
    "    \"agent\",\n",
    "    should_continue,\n",
    "    {\n",
    "        \"continue\": \"action\",\n",
    "        \"end\": END\n",
    "    }\n",
    ")\n",
    "\n",
    "workflow.add_edge('action', 'agent')\n",
    "\n",
    "app = workflow.compile()\n",
    "\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
    "response = app.invoke(inputs)\n",
    "\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "        +-----------+           \n",
      "        | __start__ |           \n",
      "        +-----------+           \n",
      "               *                \n",
      "               *                \n",
      "               *                \n",
      "          +-------+             \n",
      "          | agent |             \n",
      "          +-------+             \n",
      "          *        ..           \n",
      "        **           ..         \n",
      "       *               .        \n",
      "+--------+         +---------+  \n",
      "| action |         | __end__ |  \n",
      "+--------+         +---------+  \n"
     ]
    }
   ],
   "source": [
    "\n",
    "app.get_graph().print_ascii()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain import hub\n",
    "from langchain.agents import create_openai_functions_agent\n",
    "from langchain_openai.chat_models import ChatOpenAI\n",
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "tools = [TavilySearchResults(max_results=1)]\n",
    "\n",
    "# Get the prompt to use - you can modify this!\n",
    "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
    "\n",
    "# Choose the LLM that will drive the agent\n",
    "llm = ChatOpenAI(api_key=\"sk-uRUknVzF2daRmidJ227fEa256c5d4f4b9f9a7bB6CeCd0176\", \n",
    "                 base_url=\"https://api.openai-hub.com/v1\", \n",
    "                 callbacks=[handler])\n",
    "\n",
    "# Construct the OpenAI Functions agent\n",
    "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n",
    "\n",
    "from typing import TypedDict, Annotated, List, Union\n",
    "from langchain_core.agents import AgentAction, AgentFinish\n",
    "from langchain_core.messages import BaseMessage\n",
    "import operator\n",
    "\n",
    "class AgentState(TypedDict):\n",
    "    # The input string\n",
    "    input: str\n",
    "    # The list of previous messages in the conversation\n",
    "    chat_history: list[BaseMessage]\n",
    "    # The outcome of a given call to the agent\n",
    "    # Needs `None` as a valid type, since this is what this will start as\n",
    "    agent_outcome: Union[AgentAction, AgentFinish, None]\n",
    "    # List of actions and corresponding observations\n",
    "    # Here we annotate this with `operator.add` to indicate that operations to\n",
    "    # this state should be ADDED to the existing values (not overwrite it)\n",
    "    intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n",
    "    \n",
    "\n",
    "from langchain_core.agents import AgentFinish\n",
    "from langgraph.prebuilt.tool_executor import ToolExecutor\n",
    "\n",
    "# This a helper class we have that is useful for running tools\n",
    "# It takes in an agent action and calls that tool and returns the result\n",
    "tool_executor = ToolExecutor(tools)\n",
    "\n",
    "\n",
    "# Define the agent\n",
    "def run_agent(data):\n",
    "    agent_outcome = agent_runnable.invoke(data)\n",
    "    return {\"agent_outcome\": agent_outcome}\n",
    "\n",
    "\n",
    "# Define the function to execute tools\n",
    "def execute_tools(data):\n",
    "    # Get the most recent agent_outcome - this is the key added in the `agent` above\n",
    "    agent_action = data[\"agent_outcome\"]\n",
    "    response = input(f\"[y/n] continue with: {agent_action}?\")\n",
    "    if response == \"n\":\n",
    "        raise ValueError\n",
    "    output = tool_executor.invoke(agent_action)\n",
    "    return {\"intermediate_steps\": [(agent_action, str(output))]}\n",
    "\n",
    "\n",
    "# Define logic that will be used to determine which conditional edge to go down\n",
    "def should_continue(data):\n",
    "    # If the agent outcome is an AgentFinish, then we return `exit` string\n",
    "    # This will be used when setting up the graph to define the flow\n",
    "    if isinstance(data[\"agent_outcome\"], AgentFinish):\n",
    "        return \"end\"\n",
    "    # Otherwise, an AgentAction is returned\n",
    "    # Here we return `continue` string\n",
    "    # This will be used when setting up the graph to define the flow\n",
    "    else:\n",
    "        return \"continue\"\n",
    "    \n",
    "    \n",
    "from langgraph.graph import END, StateGraph\n",
    "\n",
    "# Define a new graph\n",
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "# Define the two nodes we will cycle between\n",
    "workflow.add_node(\"agent\", run_agent)\n",
    "workflow.add_node(\"action\", execute_tools)\n",
    "\n",
    "# Set the entrypoint as `agent`\n",
    "# This means that this node is the first one called\n",
    "workflow.set_entry_point(\"agent\")\n",
    "\n",
    "# We now add a conditional edge\n",
    "workflow.add_conditional_edges(\n",
    "    # First, we define the start node. We use `agent`.\n",
    "    # This means these are the edges taken after the `agent` node is called.\n",
    "    \"agent\",\n",
    "    # Next, we pass in the function that will determine which node is called next.\n",
    "    should_continue,\n",
    "    # Finally we pass in a mapping.\n",
    "    # The keys are strings, and the values are other nodes.\n",
    "    # END is a special node marking that the graph should finish.\n",
    "    # What will happen is we will call `should_continue`, and then the output of that\n",
    "    # will be matched against the keys in this mapping.\n",
    "    # Based on which one it matches, that node will then be called.\n",
    "    {\n",
    "        # If `tools`, then we call the tool node.\n",
    "        \"continue\": \"action\",\n",
    "        # Otherwise we finish.\n",
    "        \"end\": END,\n",
    "    },\n",
    ")\n",
    "\n",
    "# We now add a normal edge from `tools` to `agent`.\n",
    "# This means that after `tools` is called, `agent` node is called next.\n",
    "workflow.add_edge(\"action\", \"agent\")\n",
    "\n",
    "# Finally, we compile it!\n",
    "# This compiles it into a LangChain Runnable,\n",
    "# meaning you can use it as you would any other runnable\n",
    "app = workflow.compile()\n",
    "\n",
    "\n",
    "inputs = {\"input\": \"北京今天的天气怎么样？\", \"chat_history\": []}\n",
    "for s in app.stream(inputs):\n",
    "    print(list(s.values())[0])\n",
    "    print(\"----\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
